/**
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */

package pittypat.web;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;

import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCodes;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.websocket.Session;

import pittypat.IJson;
import pittypat.Json;

/**
 * 表示一个与客户端之间的 WebSocket 连接，用于处理功能请求或文件上传。
 * <p>
 * 此类提供与 WebSocketEndPoint 之间的交互接口。UploadSocket 和 FunctionSocket 实现该类，分别用于通过
 * WebSocket 上传文件和执行需要耗费较长时间的功能操作。
 */
public abstract class WebSocket implements AutoCloseable {
	private static final Logger LOGGER = LogManager.getLogger(WebSocket.class);

	// 当前 WebSoccket 会话
	private Session session;

	/**
	 * 使用当前会话初始化 WebSocket 的新实例。
	 * 
	 * @param session
	 *            一个 Session 对象。
	 */
	protected WebSocket(Session session) {
		this.session = session;
	}

	/**
	 * 获取当前请求上下文。
	 * 
	 * @return 一个 RequestContext 对象。
	 */
	public RequestContext getRequestContext() {
		return RequestContext.getRequestContext(this.session);
	}

	/**
	 * 获取指定名称的参数值。如果存在多个值，则总是返回第一个非空的值。
	 * 
	 * @param name
	 *            参数名称。
	 * @return 参数值。
	 */
	public String getParameter(String name) {
		Map<String, List<String>> m = this.session.getRequestParameterMap();
		List<String> list = m.getOrDefault(name, null);

		if (list == null || list.size() == 0) {
			return "";
		}

		for (String v : list) {
			if (v != null && v.length() > 0) {
				return v;
			}
		}

		return "";
	}

	/**
	 * 当连接打开时要执行的方法。默认实现总是返回 NORMAL_CLOSURE。
	 * 
	 * @return 返回一个值，该值指示操作是否已经处理完毕。如果返回值不是 0，连接将被关闭。
	 */
	protected int onOpen() {
		return CloseCodes.NORMAL_CLOSURE.getCode();
	}

	/**
	 * 由接收文本消息的继承类实现，用于处理接收到的文本消息。默认实现总是返回 NORMAL_CLOSURE。
	 * 
	 * @param text
	 *            已经接收到的文本内容。
	 * @param last
	 *            指示当前文本是否是最后一部分。
	 * @return 返回一个值，该值指示操作是否已经处理完毕。如果返回值不是 0，连接将被关闭。
	 */
	protected int onTextMessage(String text, boolean last) {
		return CloseCodes.NORMAL_CLOSURE.getCode();
	}

	/**
	 * 由接收二进制消息的继承类实现，用于处理接收到的二进制消息。默认实现总是返回 NORMAL_CLOSURE。
	 * 
	 * @param data
	 *            已经接收到的二进制数据。
	 * @param last
	 *            指示当前文本是否是最后一部分。
	 * @return 返回一个值，该值指示操作是否已经处理完毕。如果返回值不是 0，连接将被关闭。
	 */
	protected int onBinaryMessage(byte[] data, boolean last) {
		return CloseCodes.NORMAL_CLOSURE.getCode();
	}

	/**
	 * 当发生错误时执行的方法。默认记录日志，发送错误信息，关闭连接。默认实现总是返回 UNEXPECTED_CONDITION。
	 * 
	 * @return 返回一个值，该值指示是否应该关闭连接。如果返回值不是 0，连接将被关闭。
	 * @throws IOException
	 */
	protected int onError(Throwable error) throws IOException {
		// 向客户端发送错误信息
		this.sendError(error);

		// 记录错误消息
		LOGGER.error(error.getLocalizedMessage(), error);

		// 指示应该关闭连接
		return CloseCodes.UNEXPECTED_CONDITION.getCode();
	}

	/**
	 * 当连接被关闭时执行的方法。默认什么都不做。
	 */
	protected void onClose() {
		// 关闭后将 session 设置为 null，以避免重复调用 WebSocket.close
		this.session = null;
	}

	/**
	 * 关闭当前 WebSocket 连接。如果此前已经关闭，什么都不做。
	 * 
	 * @param code
	 *            关闭代码。
	 */
	public void close(int code) {
		if (this.session == null) {
			return;
		}

		try {
			this.session.close(new CloseReason(CloseReason.CloseCodes.getCloseCode(code), null));
		} catch (Exception e) {
			LOGGER.error(e.getLocalizedMessage(), e);
		}

		// 关闭后将 session 设置为 null，以避免重复调用 WebSocket.close
		this.session = null;
	}

	/**
	 * 关闭当前 WebSocket 连接。如果此前已经关闭，什么都不做。
	 */
	public void close() {
		if (this.session == null) {
			return;
		}

		try {
			this.session.close();
		} catch (Exception e) {
			LOGGER.error(e.getLocalizedMessage(), e);
		}

		// 关闭后将 session 设置为 null，以避免重复调用 WebSocket.close
		this.session = null;
	}

	/**
	 * 将一个 IJson 值转换为字符串格式的 JSON 消息。
	 * 
	 * @param value
	 *            一个 IJson 对象。
	 * @return 要发送到客户端的 JSON 消息字符串。
	 * @throws IOException
	 */
	private String jsonToString(IJson value) throws IOException {
		String json = "";
		if (value == null) {
			// 空消息
			json = "{\"type\":\"empty\"}";
		} else {
			// 创建 JSON 字符串
			StringWriter writer = new StringWriter(1024);

			try {
				writer.write("{\"type\":\"value\",\"valueType\":\"" + Json.getTypeName(value) + "\",\"value\":");
				value.write(writer);
				writer.write('}');

				json = writer.toString();
			} finally {
				writer.close();
			}
		}

		return json;
	}

	/**
	 * 同步发送指定的值到客户端。如果发送失败，则关闭连接。
	 * 
	 * @param value
	 *            要发送的值。
	 * @throws IllegalStateException
	 *             连接已经关闭。
	 * @throws IOException
	 */
	public void send(IJson value) throws IOException {
		this.send(jsonToString(value));
	}

	/**
	 * 异步发送指定的值到客户端。如果发送失败，则关闭连接。
	 * 
	 * @param value
	 *            要发送的值。
	 * @throws IllegalStateException
	 *             连接已经关闭。
	 * @throws IOException
	 */
	public void sendAsync(IJson value) throws IOException {
		this.sendAsync(jsonToString(value));
	}

	/**
	 * 向客户端发送一个 JSON 字符串。如果发送失败，则关闭连接。
	 * 
	 * @param json
	 *            要发送的 JSON 字符串。不对字符串进行任何处理，必须是一个有效的 JSON 字符串。
	 * @throws IllegalStateException
	 *             连接已经关闭。
	 * @throws IOException
	 */
	public void send(String json) throws IOException {
		if (this.session == null) {
			throw new IllegalStateException("连接已经关闭。");
		}

		this.session.getBasicRemote().sendText(json);
	}

	/**
	 * 异步向客户端发送一个 JSON 字符串。如果发送失败，则关闭连接。
	 * 
	 * @param json
	 *            要发送的 JSON 字符串。不对字符串进行任何处理，必须是一个有效的 JSON 字符串。
	 * @throws IllegalStateException
	 *             连接已经关闭。
	 * @throws IOException
	 */
	public void sendAsync(String json) throws IOException {
		if (this.session == null) {
			throw new IllegalStateException("连接已经关闭。");
		}

		this.session.getAsyncRemote().sendText(json);
	}

	/**
	 * 发送一个错误消息到客户端。如果发送失败，则关闭连接。
	 * 
	 * @param message
	 *            要发送的消息文本。消息会在内部编码。
	 * @throws IllegalStateException
	 *             连接已经关闭。
	 * @throws IOException
	 */
	public void sendError(String message) throws IOException {
		if (this.session == null) {
			throw new IllegalStateException("连接已经关闭。");
		}

		sendError(this.session, message);
	}

	/**
	 * 发送一个错误消息到客户端。如果发送失败，则关闭连接。
	 * 
	 * @param exc
	 *            已经发生的异常。
	 * @throws IllegalStateException
	 *             连接已经关闭。
	 * @throws IOException
	 */
	public void sendError(Throwable exc) throws IOException {
		if (this.session == null) {
			throw new IllegalStateException("连接已经关闭。");
		}

		sendError(this.session, exc);
	}

	/**
	 * 发送一个错误消息到客户端。如果发送失败，则关闭连接。
	 * 
	 * @param message
	 *            要发送的消息文本。消息会在内部编码。
	 * @throws IllegalArgumentException
	 *             session 为 null。
	 * @throws IOException
	 */
	public static void sendError(Session session, String message) throws IOException {
		if (session == null) {
			throw new IllegalArgumentException("没有提供 session 参数。");
		}

		session.getBasicRemote()
				.sendText("{\"type\":\"error\",\"message\":\"" + (message == null ? "" : Json.escape(message)) + "\"}");
	}

	/**
	 * 发送一个错误消息到客户端。如果发送失败，则关闭连接。
	 * 
	 * @param exc
	 *            已经发生的异常。
	 * @throws IllegalArgumentException
	 *             session 为 null。
	 * @throws IOException
	 */
	public static void sendError(Session session, Throwable exc) throws IOException {
		if (session == null) {
			throw new IllegalArgumentException("没有提供 session 参数。");
		}

		String msg = "";
		if (exc == null) {
			// 空字符串消息
			msg = "";
		} else {
			// 创建 JSON 字符串
			if (ApiContextListener.isDebug()) {
				// debug 版本时创建详细的错误消息
				ByteArrayOutputStream stream = new ByteArrayOutputStream();
				try {
					PrintWriter writer = new PrintWriter(stream);
					try {
						// 堆栈跟踪信息
						exc.printStackTrace(writer);
						writer.flush();
						msg = stream.toString("utf-8");
					} finally {
						writer.close();
					}
				} finally {
					stream.close();
				}
			} else {
				// 发布版本时仅仅发送基本消息
				msg = exc.getLocalizedMessage();
			}
		}

		sendError(session, msg);
	}
}
